package model;

import static util.Constants.BOARD_HEIGHT;
import static util.Constants.BOARD_WIDTH;
import static util.Constants.RACK_SIZE;

import java.util.Collections;
import java.util.HashMap;

import model.iview.IBoard;
import model.iview.ITile;

import util.Coordinate;

import exceptions.BadMoveException;

/**
 * Cette classe représente le plateau de jeu avec les tuiles déjà posées.
 * 
 * Cette classe utilise le <i>pattern Singleton</i>. On ne peut donc avoir
 * qu'une seule instance du plateau pour toute l'application.
 * 
 * Le plateau est de taille {@value util.Constants#BOARD_HEIGHT}*
 * {@value util.Constants#BOARD_WIDTH}
 * 
 * @author Lucile Gramusset et Guillaume Poussel
 * 
 */
public class Board implements IBoard {

    /**
     * Bonus de base du plateau
     */
    private static final Bonus[][] BOARD_BONUS = {
            { Bonus.M_TPL, Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.M_TPL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.M_TPL },
            { Bonus.NONE, Bonus.M_DBL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.M_DBL, Bonus.NONE },
            { Bonus.NONE, Bonus.NONE, Bonus.M_DBL, Bonus.NONE, Bonus.NONE,
                    Bonus.NONE, Bonus.L_DBL, Bonus.NONE, Bonus.L_DBL,
                    Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.M_DBL,
                    Bonus.NONE, Bonus.NONE },
            { Bonus.L_DBL, Bonus.NONE, Bonus.NONE, Bonus.M_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.M_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.L_DBL },
            { Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.M_DBL,
                    Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.M_DBL, Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE },
            { Bonus.NONE, Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE },
            { Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE, Bonus.NONE,
                    Bonus.NONE, Bonus.L_DBL, Bonus.NONE, Bonus.L_DBL,
                    Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.L_DBL,
                    Bonus.NONE, Bonus.NONE },
            { Bonus.M_TPL, Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.STAR, Bonus.NONE, Bonus.NONE,
                    Bonus.NONE, Bonus.L_DBL, Bonus.NONE, Bonus.NONE,
                    Bonus.M_TPL },
            { Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE, Bonus.NONE,
                    Bonus.NONE, Bonus.L_DBL, Bonus.NONE, Bonus.L_DBL,
                    Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.L_DBL,
                    Bonus.NONE, Bonus.NONE },
            { Bonus.NONE, Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE },
            { Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.M_DBL,
                    Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.M_DBL, Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.NONE },
            { Bonus.L_DBL, Bonus.NONE, Bonus.NONE, Bonus.M_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.M_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.L_DBL },
            { Bonus.NONE, Bonus.NONE, Bonus.M_DBL, Bonus.NONE, Bonus.NONE,
                    Bonus.NONE, Bonus.L_DBL, Bonus.NONE, Bonus.L_DBL,
                    Bonus.NONE, Bonus.NONE, Bonus.NONE, Bonus.M_DBL,
                    Bonus.NONE, Bonus.NONE },
            { Bonus.NONE, Bonus.M_DBL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.L_TPL, Bonus.NONE, Bonus.NONE, Bonus.NONE,
                    Bonus.M_DBL, Bonus.NONE },
            { Bonus.M_TPL, Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.M_TPL, Bonus.NONE,
                    Bonus.NONE, Bonus.NONE, Bonus.L_DBL, Bonus.NONE,
                    Bonus.NONE, Bonus.M_TPL } };

    private static Board boardInstance;

    /**
     * Retourne l'instance du plateau de jeu à utiliser. Cette méthode est à la
     * base du <i>pattern Singleton</i>.
     * 
     * @return L'unique instance de la classe {@code Board}
     */
    public static Board getInstance() {
        if (boardInstance == null)
            boardInstance = new Board();
        return boardInstance;
    }

    /**
     * Tuiles posées sur le plateau
     */
    private Tile[][] tiles;

    /**
     * Crée un nouveau plateau de jeu, vide.
     */
    private Board() {
        tiles = new Tile[BOARD_HEIGHT][BOARD_WIDTH];
    }

    /* (non-Javadoc)
     * @see model.IBoard#getBonus(util.Coordinate)
     */
    public Bonus getBonus(Coordinate coord) {
        return this.getBonus(coord.getLigne(), coord.getColonne());
    }

    /**
     * Retourne le bonus situé sur une case donnée
     * 
     * @param row
     *            Ligne de la case
     * @param col
     *            Colonne de la case
     * @return Le bonus situé sur la case
     */
    protected Bonus getBonus(int row, int col) {
        return BOARD_BONUS[row][col];
    }

    /* (non-Javadoc)
     * @see model.IBoard#getScore(util.Coordinate, util.Coordinate)
     */
    public int getScore(Coordinate start, Coordinate end) {
        if (start.equals(end)
                || (start.getLigne() == end.getLigne() && start.getColonne() == end
                        .getColonne()))
            return 0;

        int score = 0;
        int coefficient = 1;

        if (start.getLigne() != end.getLigne()) {
            for (int i = start.getLigne(); i <= end.getLigne(); i++) {
                ITile tile = getTile(i, start.getColonne());
                Bonus bonus = getBonus(i, start.getColonne());
                if (bonus == Bonus.L_DBL) {
                    score += tile.getScore() * 2;
                } else if (bonus == Bonus.L_TPL) {
                    score += tile.getScore() * 3;
                } else {
                    score += tile.getScore();
                    if (bonus == Bonus.M_DBL || bonus == Bonus.STAR) {
                        coefficient *= 2;
                    } else if (bonus == Bonus.M_TPL) {
                        coefficient *= 3;
                    }
                }

            }
        } else {
            for (int i = start.getColonne(); i <= end.getColonne(); i++) {
                ITile tile = getTile(start.getLigne(), i);
                Bonus bonus = getBonus(start.getLigne(), i);

                if (bonus == Bonus.L_DBL) {
                    score += tile.getScore() * 2;
                } else if (bonus == Bonus.L_TPL) {
                    score += tile.getScore() * 3;
                } else {
                    score += tile.getScore();
                    if (bonus == Bonus.M_DBL || bonus == Bonus.STAR) {
                        coefficient *= 2;
                    } else if (bonus == Bonus.M_TPL) {
                        coefficient *= 3;
                    }
                }
            }
        }

        return score * coefficient;
    }

    /* (non-Javadoc)
     * @see model.IBoard#getTile(util.Coordinate)
     */
    public Tile getTile(Coordinate coord) {
        return this.getTile(coord.getLigne(), coord.getColonne());
    }

    /**
     * Retourne la tuile présente sur le plateau.
     * 
     * @param row
     *            Ligne de la tuile
     * @param col
     *            Colonne de la tuile
     * @return La tuile présente sur le plateau, <code>null</code> si il n'y en
     *         a pas.
     */
    public Tile getTile(int row, int col) {
        return this.tiles[row][col];
    }

    /* (non-Javadoc)
     * @see model.IBoard#hasTile(util.Coordinate)
     */
    public boolean hasTile(Coordinate coord) {
        return this.hasTile(coord.getLigne(), coord.getColonne());
    }

    /**
     * Détermine si une tuile est présente sur le plateau
     * 
     * @param row
     *            Ligne de la case
     * @param col
     *            Colonne de la case
     * @return <code>true</code> si il y a une tuile sur cette case,
     *         <code>false</code> sinon.
     */
    public boolean hasTile(int row, int col) {
        return this.tiles[row][col] != null;
    }

    /**
     * Pose une tuile sur le plateau.
     * 
     * @param coord
     *            Emplacement de la tuile
     * @param tile
     *            Tuile à poser
     */
    private void put(Coordinate coord, Tile tile) {
        this.set(coord.getLigne(), coord.getColonne(), tile);
    }

    /**
     * Pose des tuiles sur le plateau
     * 
     * @param letters
     *            Table de correspondance entre les tuiles et leur emplacement
     * @param horizontal
     *            Indique si les lettres sont posées dans le sens horizontal, ou
     *            non.
     * @return Le score formé par ces lettres. Si le nombre de lettres est égal
     *         à la taille d'un rack, un bonus de 50 points est ajouté.
     */
    protected int put(HashMap<Coordinate, Tile> letters, boolean horizontal) {
        int totalScore = 0;
        Coordinate min = Collections.min(letters.keySet());
        Coordinate max = Collections.max(letters.keySet());

        for (Coordinate c : letters.keySet()) {
            put(c, letters.get(c));
            if (horizontal) {
                totalScore += getScore(toNorth(c), toSouth(c));

            } else {
                totalScore += getScore(toWest(c), toEast(c));
            }
        }

        int scoreMot = (horizontal ? getScore(toWest(min), toEast(max))
                : getScore(toNorth(min), toSouth(max)));

        for (Coordinate c : letters.keySet())
            setUsed(c);
        return totalScore + scoreMot + (letters.size() == RACK_SIZE ? 50 : 0);
    }

    /**
     * Pose une tuile sur le plateau.
     * 
     * @param row
     *            Ligne de la tuile
     * @param col
     *            Colonne de la tuile
     * @param tile
     *            Tuile à poser
     */
    private void set(int row, int col, Tile tile) {
        this.tiles[row][col] = tile;
    }

    /**
     * Indique qu'un bonus est utilisé. Il ne comptera plus en tant que bonus.
     * 
     * @param coord
     *            L'emplacement du bonus utilisé.
     */
    protected void setUsed(Coordinate coord) {
        BOARD_BONUS[coord.getLigne()][coord.getColonne()] = Bonus.USED;
    }

    /**
     * Essaie de se déplacer vers la droite à partir d'une case jusqu'à tomber
     * au bord du plateau ou sur une case vide.
     * 
     * @param coord
     *            La case de départ
     * @return La dernière case non-vide du plateau
     */
    protected Coordinate toEast(Coordinate coord) {
        Coordinate current = coord;
        while (current.east() != null && hasTile(current.east())) {
            current = current.east();
        }
        return current;
    }

    /**
     * Essaie de remonter en partant d'une case jusqu'à tomber au bord du
     * plateau ou sur une case vide.
     * 
     * @param coord
     *            La case de départ
     * @return La dernière case non-vide du plateau
     */
    protected Coordinate toNorth(Coordinate coord) {
        Coordinate current = coord;
        while (current.north() != null && hasTile(current.north())) {
            current = current.north();
        }
        return current;
    }

    /**
     * Essaie de descendre en partant d'une case jusqu'à tomber au bord du
     * plateau ou sur une case vide.
     * 
     * @param coord
     *            La case de départ
     * @return La dernière case non-vide du plateau
     */
    protected Coordinate toSouth(Coordinate coord) {
        Coordinate current = coord;
        while (current.south() != null && hasTile(current.south())) {
            current = current.south();
        }
        return current;
    }

    public String toString() {
        StringBuffer string = new StringBuffer();
        string.append("   ");
        for (int i = 0; i < BOARD_HEIGHT; i++) {
            string.append((i < 10 ? " " : "") + i + " ");
        }
        string.append("\n");
        for (int i = 0; i < BOARD_HEIGHT; i++) {
            string.append((i < 10 ? " " : "") + i);
            for (int j = 0; j < BOARD_WIDTH; j++) {
                string
                        .append("  "
                                + (tiles[i][j] != null ? tiles[i][j]
                                        .getLetter() : "-"));
            }
            string.append("\n");
        }
        return string.toString();
    }

    /**
     * Essaie de se déplacer vers la gauche à partir d'une case jusqu'à tomber
     * au bord du plateau ou sur une case vide.
     * 
     * @param coord
     *            La case de départ
     * @return La dernière case non-vide du plateau
     */

    protected Coordinate toWest(Coordinate coord) {
        Coordinate current = coord;
        while (current.west() != null && hasTile(current.west())) {
            current = current.west();
        }
        return current;
    }

    /**
     * Détermine le mot contenu entre deux cases du plateau
     * 
     * @param start
     *            Case de départ
     * @param end
     *            Case d'arrivée
     * @exception BadMoveException
     *                Si les deux cases ne sont pas alignées (sur la même ligne
     *                ou la même colonne).
     * @return Le mot formé par la concaténation de chaque tuile entre {@code
     *         start} et {@code end}.
     */
    protected String word(Coordinate start, Coordinate end) {
        if (start.getLigne() != end.getLigne()
                && start.getColonne() != end.getColonne())
            throw new BadMoveException();
        if (start.equals(end))
            return getTile(start).getLetter().toString();
        StringBuffer sb = new StringBuffer();
        if (start.getLigne() != end.getLigne()) {
            for (int i = start.getLigne(); i <= end.getLigne(); i++) {
                sb.append(getTile(i, start.getColonne()).getLetter());
            }
        } else {
            for (int i = start.getColonne(); i <= end.getColonne(); i++) {
                sb.append(getTile(start.getLigne(), i).getLetter());
            }
        }
        return sb.toString();
    }

    /**
     * Retourne le mot formé à droite d'une case donnée
     * 
     * @param coord
     *            La case
     * @return Le mot formé à droite de la case à l'emplacement {@code coord}.
     */
    public String wordToEast(Coordinate coord) {
        Coordinate eastEnd = toEast(coord);
        if (eastEnd.equals(coord))
            return "";
        else
            return word(coord.east(), eastEnd);
    }

    /**
     * Retourne le mot formé au-dessus d'une case donnée
     * 
     * @param coord
     *            La case
     * @return Le mot formé au-dessus de la case à l'emplacement {@code coord}.
     */
    public String wordToNorth(Coordinate coord) {
        Coordinate northStart = toNorth(coord);
        if (northStart.equals(coord))
            return "";
        else
            return word(northStart, coord.north());
    }

    /**
     * Retourne le mot formé en dessous d'une case donnée
     * 
     * @param coord
     *            La case
     * @return Le mot formé en dessous de la case à l'emplacement {@code coord}.
     */
    public String wordToSouth(Coordinate coord) {
        Coordinate southEnd = toSouth(coord);
        if (southEnd.equals(coord))
            return "";
        else
            return word(coord.south(), southEnd);
    }

    /**
     * Retourne le mot formé à gauche d'une case donnée
     * 
     * @param coord
     *            La case
     * @return Le mot formé à gauche de la case à l'emplacement {@code coord}.
     */
    public String wordToWest(Coordinate coord) {
        Coordinate westStart = toWest(coord);
        if (westStart.equals(coord))
            return "";
        else
            return word(westStart, coord.west());
    }

}
